package logger

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"time"

	"github.com/lab-online/pkg/color"
	gormLogger "gorm.io/gorm/logger"
	"gorm.io/gorm/utils"
)

// copy from https://github.com/go-gorm/gorm/blob/master/logger/logger.go

// Config logger config
type GORMLoggerConfig struct {
	SlowThreshold             time.Duration
	UseConsole                bool
	IgnoreRecordNotFoundError bool
	ParameterizedQueries      bool
	LogLevel                  gormLogger.LogLevel
}

// New initialize logger
func NewGORMLogger(config GORMLoggerConfig) gormLogger.Interface {
	return &logger{config}
}

type logger struct {
	GORMLoggerConfig
}

// LogMode log mode
func (l *logger) LogMode(level gormLogger.LogLevel) gormLogger.Interface {
	newLogger := *l
	newLogger.LogLevel = level
	return &newLogger
}

// Info print info
func (l *logger) Info(ctx context.Context, msg string, data ...any) {
	if l.LogLevel&LevelInfo != 0 {
		slog.Info(msg, slog.String("pos", utils.FileWithLineNum()), slog.Any("data", data))
		if l.GORMLoggerConfig.UseConsole {
			info.Println(append([]any{utils.FileWithLineNum(), msg}, data...)...)
		}
	}
}

// Warn print warn messages
func (l *logger) Warn(ctx context.Context, msg string, data ...any) {
	if l.LogLevel&LevelWarn != 0 {
		slog.Warn(msg, slog.String("pos", utils.FileWithLineNum()), slog.Any("data", data))
		if l.GORMLoggerConfig.UseConsole {
			warn.Println(append([]any{utils.FileWithLineNum(), msg}, data...)...)
		}
	}
}

// Error print error messages
func (l *logger) Error(ctx context.Context, msg string, data ...any) {
	if l.LogLevel&LevelError != 0 {
		slog.Error(msg, slog.String("pos", utils.FileWithLineNum()), slog.Any("data", data))
		if l.GORMLoggerConfig.UseConsole {
			errorLogger.Println(append([]any{utils.FileWithLineNum(), msg}, data...)...)
		}
	}
}

// Trace print sql message
//
//nolint:cyclop
func (l *logger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
	if l.LogLevel <= 0 {
		return
	}

	elapsed := time.Since(begin)
	switch {
	case err != nil && l.LogLevel&LevelError != 0 &&
		(!errors.Is(err, gormLogger.ErrRecordNotFound) || !l.IgnoreRecordNotFoundError):
		sql, rows := fc()
		timeUsed := float64(elapsed.Nanoseconds() / 1e6)
		filePosition := utils.FileWithLineNum()
		msg := err.Error()

		if l.GORMLoggerConfig.UseConsole {
			errorLogger.Println(formatGormTrace(filePosition, msg, timeUsed, rows, sql, color.ColorRed))
		}
		slog.Error(
			msg, slog.String("pos", filePosition), slog.String("sql", sql),
			slog.Float64("elapsed", timeUsed), slog.Int64("rows", rows),
		)
	case elapsed > l.SlowThreshold && l.SlowThreshold != 0 && l.LogLevel&LevelWarn != 0:
		sql, rows := fc()
		slowLog := color.Bold(fmt.Sprintf("SLOW SQL >= %v", l.SlowThreshold))
		timeUsed := float64(elapsed.Nanoseconds() / 1e6)
		filePosition := utils.FileWithLineNum()

		if l.GORMLoggerConfig.UseConsole {
			warn.Println(formatGormTrace(filePosition, slowLog, timeUsed, rows, sql, color.ColorYellow))
		}
		slog.Warn(
			slowLog, slog.String("pos", filePosition), slog.String("sql", sql),
			slog.Float64("elapsed", timeUsed), slog.Int64("rows", rows),
		)
	case l.LogLevel&LevelInfo != 0:
		sql, rows := fc()
		timeUsed := float64(elapsed.Nanoseconds() / 1e6)
		filePosition := utils.FileWithLineNum()

		if l.GORMLoggerConfig.UseConsole {
			info.Println(formatGormTrace(filePosition, "", timeUsed, rows, sql, color.ColorGreen))
		}
		slog.Info(
			"gorm info", slog.String("pos", filePosition), slog.String("sql", sql),
			slog.Float64("elapsed", timeUsed), slog.Int64("rows", rows),
		)
	}
}

func formatGormTrace(file string, msg string, timeUsed float64, rows int64, sql string, levelColor int) string {
	return fmt.Sprintf(
		"\x1b[34;3;4m%s\x1b[0m \x1b[%dm%s \x1b[36m+%.3fms\x1b[34m rows: %d\n\x1b[1;%dm  sql:\x1b[0m \x1b[2m%s\x1b[0m",
		file, levelColor, msg, timeUsed, rows, levelColor, sql)
}

// ParamsFilter filter params
func (l *logger) ParamsFilter(ctx context.Context, sql string, params ...any) (string, []any) {
	if l.GORMLoggerConfig.ParameterizedQueries {
		return sql, nil
	}
	return sql, params
}
